﻿//-----------------------------------------------------------------------
// <copyright file="CsvReader.cs" company="n.a.">
//     Copyright Dave999. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------
[module: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "To be returned to .")]

namespace Csv
{
	using System;
	using System.Collections.Generic;
	/*
		Class to read Csv lines that conform to the only spec that sounds official
		see the internet.
	*/
	public class CsvReader
	{
		public CsvReader() { this.FieldPadding = false; }

		// FieldPadding will return empty fields when the line contains fewer
		public bool FieldPadding { get; set; }

		/*
			line parser 
			given a line breaks it into fields. deals with quotes and even
			multi-line records (as per the spec,, but recommended not to use).
		*/
		public bool ParseCsvLine(String line)
		{
			// this.m_isContinuation is set to true if the last line was only a partial record.
			// 
			if (!this.isContinuation) this.ClearData();		// clear internal data if continuation flag is not set

			// define an array to hold parsing info
			CsvField[] fields;

			// parse the line. done is normally true unless there is an unterminated "
			// which signifies multi-line data
			bool done = this.ParseLine(line, this.isContinuation, out fields);

			// break up the line according to the parse info. This dumps enclosing quotes
			// and replaces double double-quotes with a single double-quote (string.Empty() -> ")
			foreach (CsvField field in fields)
			{
				this.AddItem(line.Substring(field.Start, field.Length).Replace("\"\"", "\""));
			}

			// set the continuation flag for the next line
			this.isContinuation = !done;	// set flag to inform next parse to append data

			// inform the caller of the record state
			return done;
		}

		private void ClearData()
		{
			// if padding clear items already allocated (its a premature optimisation)
			if (this.FieldPadding)
				for (int idx = 0; idx < this.data.Count; ++idx) this.data[idx] = string.Empty;
			// the real work is to reset the pointer
			this.pointer = 0;
		}

		// wraps adding a string to the output 
		private void AddItem(String fragment)
		{
			// special case is the first item of a continued line
			if (this.isContinuation)
			{
				// add to the last populated list item and restore the line feed
				this.data[this.pointer - 1] += "\n" + fragment;
				this.isContinuation = false; // switch off so the next item is added
			}
			// below is the overwhelimingly normal case
			else
			{
				// list requires extending to hold this item
				if (this.pointer >= this.data.Count)
					this.data.Add(fragment);
				else
					this.data[this.pointer] = fragment;
				// advace the field counter
				++this.pointer;
			}
		}

		public String[] Data
		{
			get
			{
				if (!this.FieldPadding)
				{
					// there may be spare items in the array (premature optimisation remember)
					// at this point they are definitely not wanted so clear them.
					this.data.RemoveRange(this.pointer, this.data.Count - this.pointer);
				}
				return this.data.ToArray();
			}
		}

		int pointer = 0;
		bool isContinuation = false;
		List<String> data = new List<string>();

		bool ParseLine(String line, bool isContinuation, out CsvField[] fields)
		{
			List<CsvField> fieldList = new List<CsvField>();
			CsvField currentField = new CsvField(-1);
			// states:
			// 0 = normal parsing
			// 1 = inside a double quoted string
			// 2 = expecting next character to be an escaped double-quote
			int state = (isContinuation) ? 1 : 0; // set state depending on continuation state
			for (int i = 0; i < line.Length; ++i)
			{
				switch (line[i])
				{
					case ',':
						if (state == 0)
						{
							// cover the empty field case ,, or "","",
							if (currentField.Start == -1)
								currentField.Start = i;
							// set field length unless " has set it first
							if (currentField.Length == -1)
								currentField.Length = i - currentField.Start;
							// add to pile and clear the current field
							fieldList.Add(currentField);
							currentField = new CsvField(-1);
						}
						break;
					case '"':
						switch (state)
						{
							case 0: // left quote at start or following ,
								state = 1;  // set state to be in quoted literal
								break;
							case 1: // in quotes
								if (currentField.Start == -1)
									currentField.Start = i;
								// look ahead to test for second "
								if (i < line.Length - 1 && line[i + 1] == '"')
									state = 2;
								else
								{
									currentField.Length = i - currentField.Start;
									state = 0;
								}
								break;
							case 2: // here only if a second quote
								state = 1;  // return state to normal literal state
								break;
						}
						break;
					default:
						if (currentField.Start == -1)
							currentField.Start = i;
						break;
				}
			}
			//if (currentField.Start != -1)
			//{
			//    if (currentField.Length == -1)
			//        currentField.Length = line.Length - currentField.Start;
			//    fieldList.Add(currentField);
			//}
			if (line.Length > 0) // trapping the blank line case in this case don't add a field.
			{
				if (currentField.Start == -1) // trapping the line ends with comma case
					currentField.Start = line.Length;
				if (currentField.Length == -1) // no terminator
					currentField.Length = line.Length - currentField.Start;
				fieldList.Add(currentField);
			}

			fields = fieldList.ToArray();

			// hopefully only state 1
			return (state == 0);
		}

		// this version is not for data with line-feeds
		private bool ParseLine_Single(string line, bool isContinuation, out CsvField[] fields)
		{
			List<CsvField> fieldList = new List<CsvField>();
			// the key to this is determining the extent of data
			// that is excluding enclosing " and ,
			// also we must allow for empty lines and empty data (eg ,, is three zero length fields)
			// the algo works best if we know that the start and length items are undefined
			// hence -1 initialisers
			CsvField currentField = new CsvField(-1);
			int state = 0;
			for (int i = 0; i < line.Length; ++i)
			{
				switch (line[i])
				{
					case ',':
						if (state == 0)
						{
							// cover the empty field case ,, or "","",
							if (currentField.Start == -1)
								currentField.Start = i;
							// set field length unless " has set it first
							if (currentField.Length == -1)
								currentField.Length = i - currentField.Start;
							// add to pile and clear the current field
							fieldList.Add(currentField);
							currentField = new CsvField(-1);
						}
						break;
					case '"':
						switch (state)
						{
							case 0: // left quote at start or following ,
								state = 1;  // set state to be in quoted literal
								break;
							case 1: // in quotes
								if (currentField.Start == -1)
									currentField.Start = i;
								// look ahead to test for second "
								if (i < line.Length - 1 && line[i + 1] == '"')
									state = 2;
								else
								{
									currentField.Length = i - currentField.Start;
									state = 0;
								}
								break;
							case 2: // here only if a second quote
								state = 1;  // return state to normal literal state
								break;
						}
						break;
					default:
						if (currentField.Start == -1)
							currentField.Start = i;
						break;
				}
			}
			if (currentField.Start != -1)
			{
				if (currentField.Length == -1)
					currentField.Length = line.Length - currentField.Start;
				fieldList.Add(currentField);
			}

			fields = fieldList.ToArray();
			return true;
		}
	}

	struct CsvField
	{
		internal int Start;
		internal int Length;

		internal CsvField(int nullValue)
		{
			this.Start = nullValue;
			this.Length = nullValue;
		}
	}

	public class CsvWriter
	{
		public static string CsvField(string field)
		{
			/* A CSV field needs to be double-quoeted if it contains:
				a) , or "
				b) a line feed character char(13) or char(10)
			*/
			bool wantQuotes = (field.IndexOf('"') != -1 || field.IndexOf(',') != -1 ||
								field.IndexOf('\n') != -1 || field.IndexOf('\r') != -1);
			if (!wantQuotes) return field;
			return "\"" + field.Replace("\"", "\"\"") + "\"";
		}

	}

}
